package com.jse;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TimerTask;
import java.util.UUID;

import javax.script.ScriptException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import javax.sql.DataSource;

//import com.zaxxer.hikari.HikariConfig;
import com.jse.cache.Cache;
import com.jse.cache.impl.LFUCache;
import com.jse.dao.Dao;
import com.jse.entity.Record;
import com.jse.ioc.Ioc;
import com.jse.ioc.loader.json.JsonLoader;
import com.jse.jdbc.Jdbcs;
import com.jse.json.Json;
import com.jse.log.Log;
import com.jse.log.Logs;
import com.jse.template.JseTemplate;
import com.jse.template.TplTemplate;
import com.jse.timer.TaskService;
import com.jse.tpl.servlet.ServletGroupTemplate;
import com.jse.util.JseMap;
import com.jse.util.MysqlBak;
import com.jse.util.Objs;
import com.jse.util.Prop;
import com.jse.util.Strings;
import com.jse.util.Times;
import com.jse.util.UrlUtil;

import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.ScriptObjectMirror;

public class JseServlet extends HttpServlet {

	Log log = Logs.get();
	Prop conf;
	Dao dao;
	Cache<Object, Object> cache;
	Ioc ioc;
	NashornScriptEngine nashornScriptEngine;
	ServletContext sc;
	String jjs_suffix;
	JseTemplate jseTemplate;

	@Override
	public void init(ServletConfig config) throws ServletException {
		ioc = new Ioc(new JsonLoader("bean.js"));
		conf = ioc.get(Prop.class,"conf");
		cache = new LFUCache<>(1000);
		dao = ioc.get(Dao.class,"dao");
		jjs_suffix = conf.containsKey("jjs.suffix") ? conf.get("jjs.suffix") : "js";
		nashornScriptEngine = ioc.get(NashornScriptEngine.class);
		sc = config.getServletContext();
		if(conf.containsKey("template")) {
			jseTemplate=(JseTemplate) Objs.create(conf.get("template"));
		}else {
			jseTemplate=new TplTemplate();
		}
		Jse.setIoc(ioc);
		Jse.setSc(sc);
		TaskService taskService = ioc.get(TaskService.class, "taskService");
		if (conf.getBoolean("bak")) {// 数据库备份
			taskService.exec(new TimerTask() {
				@Override
				public void run() {
					MysqlBak.exec(conf.get("bak.name"), conf.get("jdbc.username"), conf.get("jdbc.password"),
							conf.get("bak.path"));
					log.info("mysql bak success!");
				}
			}, Times.addDay(Times.day(conf.get("bak.time")), 1));
		}
	}

	private static final long serialVersionUID = 1L;

	@SuppressWarnings("unchecked")
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		long start_time = System.currentTimeMillis();
		String classPath = this.getClass().getResource("/").getPath();// .replace("/","\\");
		if (Jse.isWindow()) {
			classPath = classPath.substring(1);
		}
		Jse.set(req, resp);// 设置req resp
		try {
			JseMap param = getParam(req);
			if (Files.exists(Paths.get(classPath + "router.".concat(jjs_suffix)), LinkOption.NOFOLLOW_LINKS)) {
				ArrayList<Map<String, String>> list = null;
				if (cache.containsKey("routers")) {
					list = (ArrayList<Map<String, String>>) cache.get("routers");
				} else {
					List<String> ps = UrlUtil.getPaths(req.getServletPath());
					boolean is_ps = ps.stream().anyMatch(s -> Strings.isChinese(s) || Strings.isNumber(s));
					nashornScriptEngine.put("ps", is_ps ? ps : Collections.emptyList());
					nashornScriptEngine
							.eval(new String(Files.readAllBytes(Paths.get(classPath + "router.".concat(jjs_suffix)))));
					Object routers = nashornScriptEngine.invokeFunction("routers", new Object[] {});
					list = Json.fromJson(ArrayList.class, Json.toJson(routers));
					cache.put("routers", list);
				}
				Optional<Map<String, String>> router = list.stream()
						.filter((p) -> p.get("path").equals(req.getServletPath())).findFirst();
				if (!router.isPresent()) {
					fdbAction(classPath, req, resp, param);
				} else {
					routerAction(classPath, req, resp, param, router);
				}
			} else {
				fdbAction(classPath, req, resp, param);
			}

		} catch (ScriptException | NoSuchMethodException e) {
			log.error("router.js read exception:%s", e);
		} finally {
			long end_time = System.currentTimeMillis();
			log.info(" http " + req.getMethod() + ":" + req.getRequestURL() + " >>:" + (end_time - start_time) + "ms");
		}

	}

	private void routerAction(String classPath, HttpServletRequest req, HttpServletResponse resp, JseMap p,
			Optional<Map<String, String>> router) {
		String path = router.get().get("path");
		String file = router.get().get("file");
		String func = router.get().get("func");
		Object obj = null;
		try {
			obj = invokeScript(req, resp, classPath, file, func, p);
		} catch (ScriptException | IOException | NoSuchMethodException e) {
			log.warnf("router not path:%s file:%s exec:%s ip:%s path%s exception:%s", path, file, func,
					req.getRemoteAddr(), req.getContextPath(), e);
			obj = e;
		} finally {
			String page = Strings.isEmpty(router.get().get("page")) ? path.concat(".html") : router.get().get("page");
			String suffix = com.jse.util.Files.getSuffix(page);
			JseMap action = JseMap.make("page", page).add("suffix", suffix);
			render(action, req, resp, obj);
		}
	}

	private void fdbAction(String classPath, HttpServletRequest req, HttpServletResponse resp, JseMap p) {
		String path = req.getServletPath();
		JseMap action = UrlUtil.getAction(path, p, conf);
		String jjs = action.getString("jjs");
		String func = action.getString("func");
		boolean isfile = true;
		Object obj = null;
		try {
			if (conf.containsKey("jjs.db") && conf.getBoolean("jjs.db")) {// 数据库读取模式
				List<Record> res = dao.query(conf.get("jjs.table"));
				final String jjs1 = jjs;// 临时常量
				Optional<Record> re = res.stream()
						.filter((r) -> r.getString("jjs").equals(jjs1) && r.getString("func").equals(func)).findFirst();
				if (re.isPresent()) {
					isfile = false;
					nashornScriptEngine.eval(re.get().getString("script"));
				}
			}
			if (isfile) {// 未从db读取时 走文件模式
				if (!Strings.isEmpty(jjs)) {
					String jjsaction = jjs.concat(System.getProperty("file.separator")).concat(func);
					if (Files.exists(Paths.get(classPath, jjsaction + ".".concat(jjs_suffix)),
							LinkOption.NOFOLLOW_LINKS)) {// 直接映射不存在
						jjs = jjsaction;
						isfile = false;
						log.debugf(" find jjs:%s.%s exec %s", jjs, jjs_suffix, func);
					} else if (func.contains("_") && Files.exists(
							Paths.get(classPath,
									jjs + System.getProperty("file.separator") + func.substring(0, func.indexOf('_')) + ".".concat(jjs_suffix)),
							LinkOption.NOFOLLOW_LINKS)) {
						jjs = jjs + System.getProperty("file.separator") + func.substring(0, func.indexOf('_'));
						isfile = false;
						log.debugf(" find jjs:%s.%s exec %s", jjs, jjs_suffix, func);
					} else if (Files.exists(Paths.get(classPath, jjs + "/_.".concat(jjs_suffix)),
							LinkOption.NOFOLLOW_LINKS)) {
						jjs = jjs + "/_";
						isfile = false;
						log.debugf(" find jjs:%s.%s exec %s", jjs, jjs_suffix, func);
					}
				} else {
					jjs = "index";
					if (Files.exists(Paths.get(classPath, func + ".".concat(jjs_suffix)), LinkOption.NOFOLLOW_LINKS)) {// 直接映射不存在
						isfile = false;
						log.debugf(" find jjs:%s.%s exec %s", func, jjs_suffix, func);
					} else if (Files.exists(Paths.get(classPath, "index.".concat(jjs_suffix)),
							LinkOption.NOFOLLOW_LINKS)) {// 直接映射不存在
						isfile = false;
						log.debugf(" find jjs:%s.%s exec %s", "index", jjs_suffix, func);
					}
				}
			}
			if (isfile) {// 未找到router 未找到jjs文件 并且未开启混合模式
				log.warnf("not find jjs: path %s/%s.%s ip:%s prj:%s", jjs, func, jjs_suffix, req.getRemoteAddr(),
						req.getContextPath());
			} else {
				obj = invokeScript(req, resp, classPath, jjs, func, p);
			}
		} catch (ScriptException | NoSuchMethodException | IOException e) {
			log.warnf("fdb not %s:%s exec:%s ip:%s path%s exception:%s", jjs_suffix, jjs, func, req.getRemoteAddr(),
					req.getContextPath(), e);
			obj = e;
		} finally {
			render(action, req, resp, obj);
		}
	}

	private JseMap getParam(HttpServletRequest req) {
		try {
			req.setCharacterEncoding("utf-8");
		} catch (UnsupportedEncodingException e) {
			log.warnf("req param err:%s", e);
		}
		JseMap param = new JseMap();
		if (req.getContentType() != null && req.getContentType().contains("multipart/form-data")) {
			try {
				req.getParts().forEach(part -> {
					String name = part.getName();
					String fileName = getFilename(part);
					if (!Strings.isEmpty(fileName)) {
						String suffix = com.jse.util.Files.getSuffix(fileName);
						String str = req.getServletContext().getRealPath("/").replace("\\", "/");
						String appName = str.substring(0, str.lastIndexOf("/"));
						appName = appName.substring(appName.lastIndexOf("/"));
						String webPath = str.substring(0, str.indexOf("tomcat") + 7) + "webapps/";
						String filePath = str.substring(0, str.indexOf("tomcat") + 7) + "webapps/upload/file";

						SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
						String dateString = simpleDateFormat.format(new Date());
						String fid = UUID.randomUUID().toString();
						filePath = filePath + appName + "/" + dateString;
						File mulu = new File(filePath);
						if (!mulu.exists()) {
							mulu.mkdirs();
						}
						filePath = filePath + "/" + fid + ".".concat(suffix);// + fileName;
						try {
							part.write(filePath);
						} catch (IOException e) {
							log.warnf("req param file write err:%s", e);
						}
						filePath = filePath.replace(webPath, "");
						param.put(name, filePath);
					} else {
						String[] v = req.getParameterValues(name);
						param.put(name, v != null && v.length > 1 ? Arrays.asList(v) : req.getParameter(name));
					}
				});
			} catch (IOException | ServletException e) {
				log.error("req parts fail! err_msg :", e);
			}
		} else {
			req.getParameterMap().forEach((k, v) -> {
				param.put(k, v.length > 1 ? Arrays.asList(v) : v[0]);
			});
		}
		if (param.containsKey("cache") && param.getString("cache").equals("clear")) {
			cache.clear();
			log.info("req param cache=clear!");
		}
		return param;
	}

	private String getFilename(Part part) {
		String contentDispositionHeader = part.getHeader("content-disposition");
		String[] elements = contentDispositionHeader.split(";");
		for (String element : elements) {
			if (element.trim().startsWith("filename")) {
				return element.substring(element.indexOf('=') + 1).trim().replace("\"", "");
			}
		}
		return null;
	}

	private void render(JseMap action, HttpServletRequest req, HttpServletResponse resp, Object obj) {
		String page = action.getString("page");
		String suffix = action.getString("suffix");
		resp.setCharacterEncoding("utf-8");
		try {
			if (page.equals("json") || suffix.equals("json")) {// json
				if (obj instanceof ScriptObjectMirror) {// 针对jjs的[object Array]类型转换
					ScriptObjectMirror som = ScriptObjectMirror.class.cast(obj);
					if (som.toString().equals("[object Array]")) {
						obj = Json.fromJson(Json.toJson(som));
					} else if (som.toString().equals("[object Object]")) {
						obj = JseMap.WRAP(som);
					}
				}
				resp.setContentType("application/json;charset=utf-8");
				resp.getWriter().write(Json.toJson(obj));
				return;
			} else if (page.equals("jsonp") || suffix.equals("jsonp")) {// json
				if (obj instanceof ScriptObjectMirror) {// 针对jjs的[object Array]类型转换
					ScriptObjectMirror som = ScriptObjectMirror.class.cast(obj);
					if (som.toString().equals("[object Array]")) {
						obj = Json.fromJson(Json.toJson(som));
					} else if (som.toString().equals("[object Object]")) {
						obj = JseMap.WRAP(som);
					}
				}
				resp.setContentType("text/plain;charset=utf-8");
				String callback = "callback";
				StringBuffer jsonp = new StringBuffer(callback);
				jsonp.append("(").append(Json.toJson(obj)).append(")");
				resp.getWriter().write(jsonp.toString());
				return;
			} else if (page.equals("raw") || suffix.equals("raw")) {// raw
				if (obj instanceof File) {// 文件以及下载
					File f = (File) obj;
					String filename = URLEncoder.encode(f.getName(), "utf-8");
					// resp.reset();
					resp.setHeader("Content-Disposition", "attachment; filename=" + filename);
					// 获取响应报文输出流对象
					ServletOutputStream out = resp.getOutputStream();
					out.write(Files.readAllBytes(Paths.get(f.getAbsolutePath())));
					out.flush();
					out.close();
					return;
				} else {
					resp.setContentType("text/plain;charset=utf-8");
					resp.getWriter().write(obj != null ? obj.toString() : "");
					return;
				}
			} else if (page.equals("xml") || suffix.equals("xml")) {// xml
				resp.setContentType("text/xml;charset=utf-8");
				if (obj == null) {
					resp.getWriter().write("");
					return;
				} else {
					// resp.getWriter().write(Xmls.toXml(obj));
					return;
				}
			} else if (page.equals("txt") || suffix.equals("txt")) {// raw
				resp.setContentType("text/plain;charset=utf-8");
				resp.getWriter().write(obj != null ? obj.toString() : "");
				return;
			} else if (page.equals("_") || page.equals("void")) {// jpg
				return;
			} else if (req.getAttribute("_httpurl") != null) {
				String httpurl = req.getAttribute("_httpurl").toString();
				if (httpurl.startsWith("http")) {
					resp.sendRedirect(httpurl);
					return;
				} else if (httpurl.equals("")) {
					resp.sendRedirect(req.getContextPath() + "/");
					return;
				} else {
					resp.sendRedirect(req.getContextPath() + "/" + httpurl);
					return;
				}
			}else if (req.getAttribute("void") != null) {
				return;
			} else if (obj instanceof String) {
				String rtv = obj.toString().trim();
				if (rtv.startsWith("jsp:")) {
					page = "/" + rtv.substring(4);
					String webPath = req.getServletContext().getRealPath("");
					if (java.nio.file.Files.exists(Paths.get(webPath, page), LinkOption.NOFOLLOW_LINKS)) {// .html存在
						rederHtml(page, obj, action.getMap("p"), req, resp);
						return;
					} else {
						ServletGroupTemplate.instance().render("/404.html", req, resp);
						return;
					}
				} else if (rtv.startsWith(">>:")) {
					resp.sendRedirect(req.getContextPath() + "/" + rtv.substring(3));
					return;
				} else if (rtv.startsWith("->:")) {
					req.getRequestDispatcher(req.getContextPath() + "/" + rtv.substring(3)).forward(req, resp);
					return;
				} else if (rtv.startsWith("http://") || rtv.startsWith("https://")) {
					resp.sendRedirect(rtv);
					return;
				} else if (rtv.toString().trim().toUpperCase().startsWith("<!DOCTYPE HTML>")
						|| rtv.toString().trim().toLowerCase().startsWith("<html>")) {
					String html = ServletGroupTemplate.instance().getGroupTemplate().getTemplate(rtv.toString())
							.render();
					resp.setContentType("text/html;charset=utf-8");
					resp.getWriter().print(html);
					return;
				} else if (rtv.equals("void")) {// 什么都不做
					return;
				} else {
					resp.setContentType("text/html;charset=utf-8");
					String webPath = req.getServletContext().getRealPath("");
					if (java.nio.file.Files.exists(Paths.get(webPath, page), LinkOption.NOFOLLOW_LINKS)) {// .html存在
						rederHtml(page, obj, action.getMap("p"), req, resp);
						return;
					} else {
						ServletGroupTemplate.instance().render("/404.html", req, resp);
						return;
					}
				}
			} else {
				resp.setContentType("text/html;charset=utf-8");
				String webPath = req.getServletContext().getRealPath("");
				if (java.nio.file.Files.exists(Paths.get(webPath, page), LinkOption.NOFOLLOW_LINKS)) {// .html存在
					rederHtml(page,obj,action.getMap("p"),req,resp);
					return;
				} else {
					rederHtml("/404.html", obj, action.getMap("p"), req, resp);
				}
			}
		} catch (IOException | ServletException e) {
			req.setAttribute("obj", e);
			ServletGroupTemplate.instance().render("/500.html", req, resp);
			return;
		}
	}
	
	private void rederHtml(String page,Object obj,JseMap p,HttpServletRequest req,HttpServletResponse resp) {
		if (obj instanceof ScriptObjectMirror) {// 针对jjs的[object Array]类型转换
			ScriptObjectMirror som = ScriptObjectMirror.class.cast(obj);
			if (som.toString().equals("[object Array]")) {
				obj = Json.fromJson(Json.toJson(som));
			} else if (som.toString().equals("[object Object]")) {
				obj = JseMap.WRAP(som);
			}
		}
		req.setAttribute("obj", obj);
		JseMap web=conf.makeDeep(JseMap.class, "web.");
		web.forEach((k,v)->{
			req.setAttribute(k, v);
		});
		req.setAttribute("root", req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort()+ req.getContextPath() + "/");
		req.setAttribute("cache", cache);
		req.setAttribute("p", p);
		if (obj instanceof Exception) {
			if (obj.toString().contains("java.lang.NoSuchMethodException: No such function")) {
				ServletGroupTemplate.instance().render("/404.html", req, resp);
				return;
			} else {
				ServletGroupTemplate.instance().render("/500.html", req, resp);
				return;
			}
		} 
		jseTemplate.render(page, req, resp);
	}

	private Object invokeScript(HttpServletRequest req,HttpServletResponse resp,String path,String jjs,String func,JseMap p) throws NoSuchMethodException, ScriptException, IOException {
		nashornScriptEngine.put("conf", conf);
		nashornScriptEngine.put("req", req);
		nashornScriptEngine.put("resp", resp);
		nashornScriptEngine.put("session", new JseSession(req, resp));
		nashornScriptEngine.put("app", req.getServletContext());
		nashornScriptEngine.put("conf", conf);
		nashornScriptEngine.put("ioc", ioc);
		nashornScriptEngine.put("dao", dao);
		nashornScriptEngine.put("log", log);
		nashornScriptEngine.put("cache", cache);
		nashornScriptEngine.put("jsePath", path);
		nashornScriptEngine.put("domian", UrlUtil.getDomain(req.getScheme()+"://"+req.getServerName()));
		nashornScriptEngine.eval(new InputStreamReader(this.getClass().getResourceAsStream("/jse.".concat(jjs_suffix))));
		if (Files.exists(Paths.get(path + "_.".concat(jjs_suffix)), LinkOption.NOFOLLOW_LINKS))
			nashornScriptEngine.eval(new String(Files.readAllBytes(Paths.get(path + "_.".concat(jjs_suffix)))));
		if(jjs.endsWith(".".concat(jjs_suffix))) {
			nashornScriptEngine.eval(new String(Files.readAllBytes(Paths.get(path + jjs))));
		}else {
			nashornScriptEngine.eval(new String(Files.readAllBytes(Paths.get(path + jjs + ".".concat(jjs_suffix)))));
		}
		
		return nashornScriptEngine.invokeFunction(func, p);
	}
	
	@Override
	public void destroy() {
		Jse.set(null, null);
	}
}
